苦しんで覚えるC言語 15章 ポインタ変数の仕組み P280 – P292

15.1 メモリの仕組み

C言語の最大の山場は、ポインタの理解が複雑な点です。
ここで挫折するエンジニアが多いのも事実ですが、一度理解してしまうと難しいと思っていたのは、思い込みであるとも思えるほどです。

ポインタを解説する前準備として、メモリとは何か?どの様に扱われているのか?を知っておく必要があります。

参考書に記載されている文面は、敢えてここで記載はしませんので、P280~P282は各位で読破願います。ここでは要点のみ解説をしたいと思います。

メモリとソフトウェアとしての概念

メモリはデータを記憶する媒体になります。
記憶する単位としては、1byte(=8bit)を一つの纏まりとして表現することが主です。

PCのスペックで語られるメモリは、この記憶単位が8bit、16bit、32bit、64bitと進化してきたもので、これまで変数を処理するプログラムを見てきましたが、一度に64bitのデータを処理するのと、8bitのデータしか処理できないのとでは、処理能力に差があることは明らかです。

64bit処理のPCが1度で済ませることができる処理に対して、8bit処理のPCでは8回かかるわけですから、処理をすればするほど効率差が積もりあがっていくことになります。

8bitマイコンとか16bitマイコンと呼ばれるのは、上述の様な演算時の処理サイズが違うことを指しており、処理能力が異なることを意味しています。

15.2 変数とメモリの関係

変数はメモリ上に存在します。
ローカル変数とグローバル変数の解説の際に、同じ名前でも実態としては別物として処理されていたことを思い出してください。

これは、変数が配置されているメモリアドレスが異なるからです。
メモリアドレスという単語が出てきましたが、各メモリにはそれぞれアドレスが付与されています。

書籍などでよく例として言われるのは、メモリの住所(=番地)がそれぞれ用意されており、
アクセスする際には住所を指定しなければなりません。というものです。

折角なので、これから使用予定のPICマイコンのメモリを見ながら解説したいと思います。

これは、PIC16F16148のメモリの一部を抜粋したものです。
縦方向はアドレスになっており、左端に16進数で書かれた数値がアドレス値になります。

最左上が000hとなり、メモリの開始地点になります。
その下が00Bhとなり、000~00Bhまでが「Core Registers」と呼ばれる主要なレジスタが配置された領域になります。

00Bhは、10進数で11なので0~11までが配置された範囲になります。
単位はbyteになりますので、11byte分「Core Registers」が用意されていることになります。

この要領で続けて見てみましょう。
00Bhの下は00Chで「PORTA」と書かれています。
10進数で見ると、11~12byteが「PORTA」と呼ばれる媒体が配置されています。
11~12byteということは、1byteだけ「PORTA」に割かれたということなので、
言い方を変えれば、「PORTA」は1byteの媒体である。ということです。

PICマイコンの実務で改めて解説しますが、PORTAは名前の通り、PORTA制御をする際にアクセスするレジスタになります。

この様な要領でアドレスを下まで読み続けていくと、07Fhが最後になります。
つまり、000h~07Fhがこのメモリの全体範囲ということです。

これまで解説してきたプログラムの変数も、同じ様に連続したアドレス番地に配置されます。
適当な例となりますが、1byte変数(char型)であれば000h、4byte変数(int型)であれば、001h~004hといった感じで、変数のメモリサイズに応じてアドレスが割り当てされます。

PICのメモリでは、予め「PORTA」というレジスタ名が割り当てされていましたが、コンパイラが決めた番地に対して、自分で名付けた変数名が配置される様なイメージです。

ここでは詳細を割愛しますが、連続したアドレス番地に割り当てされるかどうかは、条件によります。
上記の適当な例は、000から004hまで連続した配置で説明をしていますが、
000hに1byte変数、010hに別の1byte変数といった感じで、歯抜けで配置される可能性もあるため、必ず連続しているアドレス番地であるとは言い切れません。

メモリ上の番号を表示する

上述の内容が概念として理解いただければ、以下の参考書プログラムコードの意味は難しくないと思います。

#include <stdio.h>

int main(void)
{
    int i;
    printf("%p\n",&i);

    return 0;
}

main処理は3行しかありません。
ポイントはprintf関数になります。

%pというのは、printf関数の仕様を調べると「ポインタ型として出力する」といった説明が出てきます。
何のことかよくわからない・・・。ということになるので、変数のアドレスを16進数で表示させるものと覚えてください。
これなら理解できると思います。

&iという記述が出てきます。
iは int i の変数 i です。
&は、C言語で用意されたアドレス演算子と呼ばれるもので、変数のアドレスを取得する場合に使う記号と覚えておいてください。

そうすると、printf(“%p\n”,&i); は、&iとして記載することでint i の変数アドレスを取得し、%pを用いて16進数で表示する。という処理になります。

int i はmain関数内のローカル変数になります。
ローカル変数であれ、グローバル変数であれ、変数は必ずどこかのメモリに配置されていますので、配置されたメモリのアドレスが存在します。

参考書では実行結果が、0012FF80 となっていますので、この値がint i 変数が置かれているアドレスになります。
参考書にも記載されていますが、このアドレスは各位のパソコンや開発環境によって変わります。
必ず同じメモリアドレス番地に配置されるわけではないので、ご自身で実行した際に違う値になることをご周知ください。

今後、プログラムを扱う上で2進数、10進数、16進数は度々登場します。
参考書の初期の頃にも解説したと思いますが、8進数はあまり使いません。

複数の変数の番号

ここまで一つだけの変数に対してアドレスの関係性を解説しました。
変数が複数になった場合も同じです。
参考書のコードを例に解説してみます。

#include <stdio.h>

int main(void)
{
    int i1,i2,i3;

    printf("i1(%p)\n",&i1):
    printf("i2(%p)\n",&i2):
    printf("i3(%p)\n",&i3):

    return 0;
}

int型で i1、i2、i3と3つ変数が宣言され、各変数のアドレスをprintf関数の%pで確認したものです。
参考書の実行結果では、以下になっているようです。
i1・・・0012FF78
i2・・・0012FF7C
i3・・・0012FF80

16進数なのですが、末尾の2桁をみれば、78⇒7C⇒80と推移しており、
10進数にすると4ずつ増えており、int型は4byteなので、4byteずつメモリの領域が確保されていることが分かります。
つまり、ここでは3つの変数は連続したアドレスに配置されたということです。

ちなみに、コンパイラによってはint型が2byteで処理されるものがあります。
このコンパイラの場合、アドレスは2byteずつ確保されて配置されます。

配列のアドレス番号

変数の纏まりである配列で見ても、同じになります。
配列の場合は、指定要素数については纏まった範囲で配置されますので歯抜けになることはありません。

#include <stdio.h>

int main(void)
{
    int array[10];

    printf("array___(%p)\n",array);
    printf("array[0](%p)\n",&array[0]);
    printf("array[1](%p)\n",&array[1]);
    printf("array[2](%p)\n",&array[2]);

    return 0;
}

参考書の結果は以下となっています。

array ・・・0012FF5C
array[0[・・・0012FF5C
array[1]・・・0012FF60
array[2]・・・0012FF64

配列の各要素のメモリアドレス番号が表示されています。
int型の配列なので、各要素は4byteです。
なので、4byteずつアドレス値が増えていますので、配列としては連番で割り当てされたということです。

& 付の変数の正体

過去の解説で参照してきたプログラムでは、変数に & は付与されていませんでした。
つまり、変数のアドレスを取得するのではなく、変数の値を取得したいので & は付けていなかったと理解してください。

変数の前に & を付けると変数のアドレス値を取得する
何もつけない場合は、変数の値を取得する。

この2行はポインタの全てです。
この2行で書かれていることは何も難しくありません。
この2行を意識してポインタを見ていけば、ポインタは簡単に理解できます。

すべては値渡しである

上記の2行がポインタの極意。
これを踏まえて以下の解説を拝読ください。

関数の引数は、変数名の前に & が付いたものはこれまで参考コードで出現することはありませんでした。
つまり、関数の引数はアドレス値ではなく、変数の中身の値を関数にコピーして渡してきたということです。これを値渡しと言います。
要点は、関数には引数の値の中身はコピーされた値である。という点です。

コピーされた値なので、引数の変数に代入をしても中身は更新されません。

#include <stdio.h>
int main(void)
{
 int atai_1;
 
 atai_1 = 100;
    
  test(atai_1);

 printf("atai_1=%d\n",atai_1);

  return 0;
}

int test(int atai_2)
{
   atai_2 = 10;
 
  printf("atai_2=%d\n",atai_2);
 
   return atai_2;
}

main関数からtest関数にatai_1を渡します。
test関数の中で引数のatai_2に10を代入してみます。

atai_2はmain関数で100が入ったatai_1を渡しているのですが、
atai_1は10にはなりません。
これが、関数の引数はコピーが渡されているという実態になります。

値渡しに対して、参照渡しというのがあります。
これは、& を付与して変数のアドレスを引数に渡すものです。

ポインタで混乱するのはここです。
変数の値が渡されたのか?
変数のメモリアドレスが渡されたのか?
開発環境の画面上は数値の羅列が表示されており、どちらの数値を表しているのか?が分からなくなり、混乱になるのです。

なので、上記の2行を思い出してください。

変数の前に & を付けると変数のアドレス値を取得する
何もつけない場合は、変数の値を取得する。

関数の引数も同じです。
& が付いているのか付いていないのか。
渡されて処理されている数値は、中身の値なのか?メモリアドレスの値なのか?
ここが整理できればポインタはクリアしたも同然です。

一様、参考書には配列の要素に対してアドレス取得をするコード例があるので解説します。

#include <stdio.h>

int main(void)
{
    char str[256];

    scanf("%s",&str[0]);

    pritf("%s\n",str);

    return 0;
}

scanf関数は標準ライブラリになり、キーボードの入力値を取得する関数です。
ポイントは、&str[0] の部分で、str配列の先頭要素であるstr[0]に対して、
&が付いていますので、str[0]のメモリアドレスを取得する。という処理になります。

キーボードと限定して書きましたが、正しい表現は標準入力から得られた値ということになります。本来はキーボードと限定して書くと正しくないのですが、理解を優先してキーボードと記載しています。

少し難しいかもしれませんが、str[0]のアドレスを取得し、
そこから入力値が格納されたstr配列の中身を%s演算子で取得しています。

つまり、入力された値の取得開始位置を&str[0]として指定をして、
そこからstr[256]までに入力された値を取得しています。

次の処理では、printf関数でstr配列の中身を%sで表示させていますので、
str[256]に入っている値を表示させています。

scanf関数の&str[0]を&str[1]にすると、最初に入力された1文字目は無視されて処理されません。
&str[2]と書くと、1文字目、2文字目は無視されて処理されません。
こう書くと、何となくイメージが湧きやすいのではないでしょうか。